Jerry's Log

File Descriptor

contents

리눅스(Linux), macOS 및 기타 유닉스(UNIX) 계열 운영체제에는 아주 유명한 철학이 있습니다: "모든 것은 파일이다(Everything is a file)."

여러분의 프로그램이 텍스트 파일을 읽든, 네트워크 연결을 통해 데이터를 보내든, 키보드에서 입력을 받든, USB 장치와 통신하든 간에 운영체제는 이 모든 상호작용을 완전히 동일한 방식으로 처리합니다.

이 수많은 연결들을 추적하고 관리하기 위해 운영체제는 파일 디스크립터(File Descriptors, FD)를 사용합니다.

파일 디스크립터가 정확히 무엇인지, 내부적으로 어떻게 작동하는지, 그리고 운영체제가 이를 어떻게 관리하는지에 대한 아주 상세한 분석입니다.


1. 핵심 개념: 파일 디스크립터란 무엇인가?

파일 디스크립터는 단순히 음수가 아닌 정수(0, 1, 2, 3 등)입니다.

프로그램이 운영체제(OS)에게 파일, 네트워크 소켓, 또는 하드웨어 장치를 열어달라고 요청하면, OS는 연결을 설정하는 모든 복잡한 작업을 뒤에서 처리합니다. 그리고 프로그램에게 원시 메모리 주소나 하드웨어 세부 정보를 주는 대신, 단순한 정수—일종의 "대기표 번호"—를 하나 건네줍니다.

여러분의 프로그램은 앞으로의 모든 작업에 이 대기표 번호(FD)를 사용합니다.


2. "마법의 3": 표준 스트림 (Standard Streams)

새로운 프로세스(실행 중인 프로그램)가 시작될 때마다, OS는 기본적으로 3개의 파일 디스크립터를 자동으로 엽니다. 여러분은 매일 이걸 무의식적으로 사용하고 있습니다.

파일 디스크립터 (FD) 약어 이름 목적
0 stdin 표준 입력 (Standard Input) 프로그램이 입력을 읽어 들이는 곳 (기본값: 키보드).
1 stdout 표준 출력 (Standard Output) 프로그램이 일반적인 출력 결과를 쓰는 곳 (기본값: 터미널 화면).
2 stderr 표준 에러 (Standard Error) 프로그램이 에러 메시지를 쓰는 곳 (기본값: 터미널 화면).

만약 Python 스크립트를 작성하고 print("Hello")를 실행하면, Python은 내부적으로 OS에게 이렇게 말합니다: "문자열 'Hello'를 파일 디스크립터 1번(표준 출력)에 써줘."


3. 아키텍처: 세 개의 테이블 (설계의 묘미)

파일 디스크립터를 진정으로 이해하려면 OS가 이를 어떻게 추적하는지 알아야 합니다. OS는 단순히 하나의 목록만 쓰지 않고, 고도로 공학적인 3단계 아키텍처를 사용합니다.

A. 프로세스 파일 디스크립터 테이블 (프로세스별 독립)

실행 중인 모든 프로그램(프로세스)은 자신만의 독립적인 배열(테이블)을 갖습니다.

B. 시스템 전체 열린 파일 테이블 (시스템 공유)

커널(운영체제의 핵심)은 시스템 전체의 모든 열린 파일을 관리하기 위해 거대한 테이블을 딱 하나 유지합니다. 파일을 열면 여기에 항목이 생성됩니다. 이 항목에는 다음이 포함됩니다:

C. i-node 테이블 (시스템 공유)

이 테이블은 하드 드라이브나 네트워크 인터페이스에 존재하는 실제 물리적 파일을 나타냅니다. i-node에는 다음이 포함됩니다:


4. 왜 굳이 세 개의 테이블을 쓸까요? (설계의 천재성)

이렇게 분리하는 것이 복잡해 보일 수 있지만, 사실 아주 복잡한 문제들을 극도로 우아하게 해결해 줍니다:

시나리오 1: 독립적으로 같은 파일 열기 프로세스 A와 프로세스 B가 각각 독립적으로 database.txt를 열면, OS는 시스템 전체 열린 파일 테이블에 두 개의 독립적인 항목을 만듭니다. 하지만 이 두 항목은 동일한 i-node를 가리킵니다.

시나리오 2: 프로세스 포크 (부모와 자식 프로세스) 어떤 프로세스가 자식 프로세스를 생성하면(fork() 사용), 자식은 부모의 FD 테이블을 그대로 복사해서 물려받습니다. 즉, 부모의 FD 4번과 자식의 FD 4번은 시스템 열린 파일 테이블의 완벽하게 똑같은 단일 항목을 가리키게 됩니다.


5. 고급 메커니즘: 리다이렉션과 파이프

FD는 결국 테이블 항목을 가리키는 단순한 정수에 불과하기 때문에, OS는 이를 이용해 기발한 마술을 부릴 수 있습니다.

출력 리다이렉션 (>)

터미널에 echo "Hello" > file.txt라고 치면, 쉘(Shell)은 다음 작업을 수행합니다:

  1. file.txt를 엽니다 (가령 FD 3번을 할당받았다고 합시다).
  2. dup2(3, 1)이라는 시스템 콜을 사용합니다. 이는 OS에게 이렇게 명령하는 것입니다: "FD 1번(표준 출력)이 FD 3번이 가리키고 있는 곳을 똑같이 가리키도록 덮어씌워라."
  3. 이제 echo 프로그램이 평소처럼 FD 1번에 출력을 내보내면, 데이터는 모니터 화면을 건너뛰고 file.txt로 직행합니다. echo 프로그램 자체는 이 사실을 전혀 모릅니다!

파이프 (|)

터미널에 ls | grep "txt"라고 치면, OS는 파이프(Pipe)를 만듭니다. 파이프는 파일처럼 작동하는 메모리 내부의 버퍼입니다. OS는 "읽기용" FD와 "쓰기용" FD, 총 두 개를 만듭니다.

  1. ls 프로그램의 FD 1번(stdout)을 파이프의 쓰기 단에 연결합니다.
  2. grep 프로그램의 FD 0번(stdin)을 파이프의 읽기 단에 연결합니다.
  3. 이제 ls의 출력 결과가 grep의 입력으로 막힘없이 흘러 들어갑니다.

6. 한계치와 "열린 파일이 너무 많음(Too Many Open Files)" 에러

열려있는 모든 파일은 커널 테이블에서 메모리를 소모하기 때문에, 운영체제는 하나의 프로세스가 동시에 열 수 있는 FD의 개수에 엄격한 제한을 둡니다.

references